<?php

/*
 * Copyright (C) 2009 - 2011 Pham Cong Dinh
 *
 * This file is part of Spica.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 3 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
include_once 'library/spica/core/utils/ContainerUtils.php';

/**
 * General file manipulation utilities.
 *
 * Facilities are provided in the following areas:
 *     * writing to a file
 *     * reading from a file
 *     * make a directory including parent directories
 *     * copying files and directories
 *     * deleting files and directories
 *     * converting to and from a URL
 *     * listing files and directories by filter and extension
 *     * comparing file content
 *     * file last changed date
 *     * calculating a checksum
 *     * read a portion of file lines based on a given line
 *
 * @see        http://commons.apache.org/io/apidocs/org/apache/commons/io/FileUtils.html#copyDirectory%28java.io.File,%20java.io.File,%20boolean%29
 *
 * @category   spica
 * @package    core
 * @subpackage utils
 * @author     Pham Cong Dinh <pcdinh at phpvietnam dot net>
 * @since      Version 0.3
 * @since      March 31, 2009
 * @copyright  Pham Cong Dinh (http://www.phpvietnam.net)
 * @license    http://www.gnu.org/licenses/lgpl-3.0.txt
 * @version    $Id: FileUtils.php 1869 2011-01-07 18:55:25Z pcdinh $
 */
class SpicaFileUtils
{
    /**
     * A mode used in moving files: If there is a existent file
     * at the destination, replace it.
     *
     * @var int
     */
    const REPLACE = 1;

    /**
     * A mode used in moving files: If there is a existent file
     * at the destination, rename it.
     *
     * @var int
     */
    const RENAME = 2;

    /**
     * Cleans non-ASCII from file name.
     *
     * @param  string $filename
     * @return string
     */
    public static function makeFileNameSafe($filename)
    {
        $ext = '';

        if (false === strpos($filename, '.'))
        {
            $ext  = substr(strrchr($filename, '.'), 1);
        }

        $base = substr($filename, 0, strlen($filename) - strlen($ext));
        $base = preg_replace("/\W/", "_", $base);
        $base = preg_replace("/[_]{2,}/", "_", $base);
        return $base.$ext;
    }

    /**
     * Returns a human-readable version of the file size,
     * where the input represents a specific number of bytes.
     *
     * @param  integer $size the number of bytes
     * @return string  a human-readable display value (includes units)
     */
    public static function byteCountToDisplaySize($size)
    {
        if ($size == '')
        {
            return false;
        }

        if ($size >= 1048576)
        {
            return round($size / 1048576 * 100) / 100 . " Mb";
        }

        if ($size >= 1024)
        {
            return round($size / 1024 * 100) / 100 . " Kb";
        }

        return $size . " b";
    }

    /**
     * Causes download of a given file. This method support partial downloads.
     *
     * @param  string $file - file's path
     * @param  string $httpFileName - name for Agent
     * @param  int    $allowPartial
     * @return string
     */
    public static function startOrResumeDownload($file, $httpFileName, $allowPartial)
    {
        $ext   = null;
        $range = 0;

        if (false === is_file($file))
        {
            return false;
        }

        $filename = basename($file);
        $ext      = strtolower(substr(strrchr($filename, "."), 1));
        $filename = str_replace(' ', '_', $httpFileName);

        if ($ext  == null || $ext === false)
        {
            $ext  = strtolower(substr(strrchr($httpFileName, "."), 1));
        }
        else
        {
            $filename = $filename.'.'.$ext;
        }

        if (strstr($_SERVER['HTTP_USER_AGENT'], "MSIE"))
        {
            $filename = preg_replace('/\./', '%de', $filename, substr_count($filename, '.') - 1);
        }

        $size = filesize($file);

        switch ($ext)
        {
            case "exe": $mime = "application/octet-stream"; break;
            case "zip": $mime = "application/zip"; break;
            case "mp3": $mime = "audio/mpeg"; break;
            case "mpg": $mime = "video/mpeg"; break;
            case "avi": $mime = "video/x-msvideo"; break;
            case "mid":
            case "midi":
            case "kar": $mime = "audio/midi"; break;
            case "pdf": $mime = "application/pdf"; break;
            case "ogg": $mime = "application/ogg"; break;
            case "wav": $mime = "audio/x-wav"; break;
            case "jpg": $mime = "image/jpeg"; break;
            case "php":
            case "htm":
            case "html":
            case "txt": return false; break;

            default: $mime = "application/force-download";
        }

        if ($allowPartial == true)
        {
            header('Cache-Control: public');
            header('Content-Disposition: attachment; filename='.$filename);
            header('Content-Type: '.$mime);
            header("Content-Transfer-Encoding: binary");
            header('Accept-Ranges: bytes');

            if (isset($_SERVER['HTTP_RANGE']))
            {
                list($a, $range) = explode('=', $_SERVER['HTTP_RANGE']);
                str_replace($range, "-", $range);
                $newLength = $size - $range;
                header('HTTP/1.1 206 Partial Content');
                header('Content-Length: '.$newLength);
                header("Content-Range: bytes $range$size/$size");
            }
            else
            {
                header("Content-Range: bytes 0-$size/$size");
                header('Content-Length: '.$size);
            }

            $fp = fopen($file, "r");
            fseek($fp, $range);

            while (!feof($fp))
            {
                set_time_limit(0);
                print(fread($fp, 1024*8));
                flush();
            }

            fclose($fp);
        }
        else
        {
            header('Content-disposition: attachment; filename='.$filename);
            header('Content-Type: '.$mime);
            header('Content-Transfer-Encoding: binary');
            header("Content-Length: ".$size);
            header("Pragma: no-cache");
            header("Expires: 0");
            readfile($file);
        }
    }

    /**
     * Moves a file to new destination.
     * Target file will get the same name if the destination file does not exist.
     *
     * @throws SpicaFileNotFoundException
     * @param  string $from
     * @param  string $to
     * @param  int    $mode
     * @return bool
     */
    public static function moveFile($from, $to, $mode = 2)
    {
        if (false === file_exists($from))
        {
            include_once 'library/spica/core/Exception.php';
            throw new SpicaFileNotFoundException('File '.$from.' does not exist.');
        }

        $dirname = dirname($to);

        switch ($mode)
        {
            case 1: // Replace
                if (true === file_exists($to))
                {
                    if (false === unlink($to))
                    {
                        throw new SpicaIOException('There is already a file at the destination file but it is unable to delete. ');
                    }
                }

            case 2: // Rename
            default:

                $filename = basename($to);
                if (true  === file_exists($to))
                {
                    $file   = self::analyzeFileName($to);
                    for ($i = 1; file_exists($to); $i++)
                    {
                        $filename = $file['base'] . '_' . $i . $file['ext'];
                        $to = $dirname.'/'.$filename;
                    }
                }
        }

        copy($from, $to);
        unlink($from);

        return $filename;
    }

    /**
     * Analyzes file into parts: path, dirname, ext, base name
     *
     * @param  string $name
     * @return array
     */
    public static function analyzeFileName($filename)
    {
        $file['path'] = $filename;
        $file['dir']  = dirname($filename);
        $filename     = basename($filename);
        $file['ext']  = (strpos($filename, '.') === false ? '' : '.' . substr(strrchr($filename, '.'), 1));
        $file['base'] = substr($filename, 0, strlen($filename) - strlen($file['ext']));

        return $file;
    }

    /**
     * Removes file extension from a file name.
     *
     * @param  string $name
     * @return string
     */
    public static function stripExtension($name)
    {
        $ext = strrchr($name, '.');

        if (false !== $ext)
        {
            $name = substr($name, 0, -strlen($ext));
        }

        return $name;
    }

    /**
     * Renames the selected file.
     *
     * @throws Pone_Exception
     * @param  string $source
     * @param  string $newName
     */
    public static function rename($source, $newName)
    {
        if (false  === file_exists($source))
        {
            throw new Pone_Exception('File '.$source.' does not exist');
        }

        $renamed   = rename($source, $newName);

        if (false  === $renamed)
        {
            $error = error_get_last();
            throw new Pone_Exception('File '.$path.' is unable to rename. '.$error['message']);
        }
    }

    /**
     * Computes the checksum of a file using the specified checksum object.
     *
     * @param  string $file
     * @return string|bool Md5 checksum {@link http://php.net/md5_file}
     */
    public static function checksum($file)
    {
        if (true === $maxsize)
        {
            return md5_file($filePath);
        }

        $size = self::getSize($filePath);

        if ($size && $size < ($maxsize * 1024) * 1024)
        {
            return md5_file($filePath);
        }

        return false;
    }

    /**
     * Copies a filtered directory to a new location preserving the file dates.
     * If $filter is NULL, the whole directory will be copied.
     *
     * The destination directory is created if it does not exist.
     * If the destination directory did exist, then this method merges the source
     * with the destination, with the source taking precedence.
     *
     * @throws
     * @param  string $srcDir  an existing directory to copy, must not be null
     * @param  string $destDir the new directory, must not be null
     * @param  SpicaFileFilter $filter the filter to apply, null means copy all
     *                         directories and files should be the same as the original
     * @param  bool   $preserveFileDate true if the file date of the copy should be the same as the original
     */
    public static function copyDirectory($srcDir, $destDir, $filter = null, $preserveFileDate = true)
    {
        // FIXME
    }

    /**
     * Deletes a directory recursively.
     *
     * @throws SpicaFileDeletionException - in case deletion is unsuccessful
     * @param  string $directory
     * @return bool true on success
     */
    public static function deleteDirectory($dir)
    {
        if (false === file_exists($dir))
        {
            throw new SpicaFileDeletionException('Directory '.$dir.' does not exist or is not accessible. ', array());
        }

        $fail = array();

        try
        {
            $it = new RecursiveDirectoryIterator($dir);
        }
        catch (Exception $ex)
        {
            $fail[] = $dir;
            throw new SpicaFileDeletionException($ex->getMessage(), $fail);
        }

        foreach (new RecursiveIteratorIterator($it, RecursiveIteratorIterator::CHILD_FIRST) as $file)
        {
            if (true === $file->isDir())
            {
                if (false === rmdir($file->getPathname()))
                {
                    $fail[] = $file->getPathname();
                }
            }
            elseif (false === unlink($file->getPathname()))
            {
                $fail[] = $file->getPathname();
            }
        }

        // Iterator still keeps a pointer to $dir making rmdir() impossible to delete $dir
        $it   = null;
        $file = null;

        if (false   === rmdir($dir))
        {
            $fail[] = $dir;
        }

        if (count($fail) > 0)
        {
            throw new SpicaFileDeletionException('Some files can not be deleted. ', $fail);
        }

        return true;
    }

    /**
     * Reads entire directory and its subdirectories and files into an partitioned array.
     *
     * @throws RuntimeException if path can not be accessible.
     * @param  string $path
     * @return array
     */
    public static function getTree($path)
    {
        $it   = new RecursiveDirectoryIterator($path);
        $tree = array();
        $dirs = array(array($it, &$tree));

        for ($i = 0; $i < count($dirs); ++$i)
        {
            $d    = &$dirs[$i][0];
            $tier = &$dirs[$i][1];

            for ($d->rewind(); $d->valid(); $d->next())
            {
                if (true === $d->isDir())
                {
                    $tier[$d->getFilename()] = array();
                    $dirs[] = array($d->getChildren(), &$tier[$d->getFilename()]);
                }
                else
                {
                    $tier[$d->getFilename()] = $d->getSize();
                }
            }
        }

        // Release file pointer
        $it = null;
        return $tree;
    }

    /**
     * When sending(for download) file via HTTP this method is safer than readChunked().
     *
     * @throws Pone_FileAccessException
     * @throws Pone_UserInterruptException
     * @see    http://vn.php.net/manual/en/features.connection-handling.php
     * @param  string $path
     * @param  int    $chunkSize 1024 bytes X 100 (100kb)
     * @return bool
     */
    public static function sendFile($path, $chunkSize = 102400)
    {
        if (false === Pone_File::isReadable($path))
        {
            throw new Pone_FileAccessException('File can not be found or accessible.');
        }

        if (CONNECTION_ABORTED === connection_status())
        {
            throw new Pone_UserInterruptException('The client disconnects gracefully (with STOP button) before the download starts', Pone_UserInterruptException::BEFORE_START);
        }

        session_write_close();
        ob_end_clean();

        // to prevent long file from getting cut off from max_execution_time
        set_time_limit(0);

        $name     = basename($path);

        // filenames in IE containing dots will screw up the
        // filename unless we add this
        if (true  === isset($_SERVER['HTTP_USER_AGENT']) && strstr($_SERVER['HTTP_USER_AGENT'], 'MSIE'))
        {
            $name = preg_replace('/\./', '%2e', $name, substr_count($name, '.') - 1);
        }

        // required, or it might try to send the serving document instead of the file
        header('Cache-Control: ');
        header('Pragma: ');
        header('Content-Type: application/octet-stream');
        header('Content-Length: ' .(string)(filesize($path)));
        header('Content-Disposition: attachment; filename="'.$name.'"');
        header("Content-Transfer-Encoding: binary\n");

        if ($file = fopen($path, "rb"))
        {
            while (false === feof($file) && CONNECTION_NORMAL === connection_status())
            {
                echo fread($file, $chunkSize);
                flush();
            }

            if (false === feof($file) && CONNECTION_ABORTED === connection_status())
            {
                fclose($file);
                throw new Pone_UserInterruptException('The client disconnects gracefully (with STOP button) before the download completes', Pone_UserInterruptException::BEFORE_COMPLETION);
            }

            fclose($file);
        }

        return ((connection_status() == CONNECTION_NORMAL) && false === connection_aborted());
    }

    /**
     * Gets file extension.
     *
     * @param  string $filename
     * @return string
     */
    public static function getExtension($filename)
    {
        return pathinfo($filename, PATHINFO_EXTENSION);
    }

    /**
     * Creates temporary file.
     *
     * @return string
     */
    public static function createTemporaryFile()
    {
        return tmpfile();
    }

    /**
     * Reads file content by chunks
     *
     * Borrow from php.net, bypass 2Mb readfile limit
     *
     * @throws Pone_Exception
     * @param  string $filename
     * @param  bool $retbytes
     * @return bool
     */
    public static function readChunked($filename, $retbytes = true)
    {
        $chunksize   = 1*(1024*1024); // how many bytes per chunk
        $buffer      = '';
        $cnt         = 0;

        $handle      = fopen($filename, 'rb');

        if ($handle  === false)
        {
            throw new Pone_Exception('File can not be opened');
        }

        while (false === feof($handle))
        {
            $buffer  = fread($handle, $chunksize);
            echo $buffer;
            ob_flush();
            flush();

            if ($retbytes)
            {
                $cnt += strlen($buffer);
            }
        }

        $status = fclose($handle);

        if ($retbytes && $status)
        {
            return $cnt; // return num. bytes delivered like readfile() does.
        }

        return $status;
    }

    /**
     * Gets file MIME
     *
     * @param  string $filePath
     * @return string
     */
    public static function getMimeForDownload($filePath)
    {
        // Get file extension
        $fileExtension = self::getExtension($filePath);

        // Get file type headers
        switch ($fileExtension)
        {
            case 'pdf':
                $ctype = 'application/pdf';
                break;

            case 'zip':
                $ctype = 'application/zip';
                break;

            case 'doc':
                $ctype = 'application/msword';
                break;

            case 'xls':
                $ctype = 'application/vnd.ms-excel';
                break;

            case 'gif':
                $ctype = 'image/gif';
                break;

            case 'png':
                $ctype = 'image/png';
                break;

            case 'jpeg':
            case 'jpg':
                $ctype = 'image/jpg';
                break;

            default:
                $ctype = 'application/force-download';
        }

        return $ctype;
    }

    /**
     * Reads a line from a file at a given line number.
     *
     * @param  string $file      The path to the file
     * @param  int    $line_num  The line number to read
     * @param  string $delimiter The character that delimits lines
     * @return string The line that is read
     */
    public static function readLine($file, $lineNum, $delimiter = "\n")
    {
        $i  = 1;
        $fp = fopen($file, 'r'); // file pointer

        while (!feof($fp))
        {
            $buffer = stream_get_line($fp, 1024, $delimiter);

            if ($i  === $lineNum)
            {
                return $buffer;
            }

            $i++;
        }

        return false;
    }

    /**
     * Returns an excerpt of a code file around the given line number.
     *
     * Ported from PONE framework.
     *
     * @param  string $file  A file path
     * @param  int    $line  The selected line number
     * @param  int    $line  The number of lines to take after the selected line
     * @param  int    $after The number of lines to take after the selected line
     * @return string An HTML string
     */
    public static function getExcerpt($file, $line, $before = 4, $after = 2)
    {
        if (true === is_readable($file))
        {
            $content = preg_split('#<br />#', highlight_file($file, true));
            $lines   = array();

            for ($i  = max($line - $before, 1), $max = min($line + $after, count($content)); $i <= $max; $i++)
            {
                $lines[] = '<li'.($i == $line ? ' class="selected"' : '').'>'.$content[$i - 1].'</li>';
            }

            return '<ol start="'.max($line - $before, 1).'">'.implode("\n", $lines).'</ol>';
        }
    }
}

/**
 * SpicaFileList represents an explicitly named list of files. FileLists are useful
 * when you want to capture a list of files regardless of whether they currently
 * exist. By contrast, FileSet operates as a filter, only returning the name of
 * a matched file if it currently exists in the file system.
 *
 * namespace spica\core\utils\FileList
 *
 * @category   spica
 * @package    core
 * @subpackage utils
 * @author     Pham Cong Dinh <pcdinh at phpvietnam dot net>
 * @since      Version 0.3
 * @since      April 08, 2009
 * @copyright  Pham Cong Dinh (http://www.phpvietnam.net)
 * @license    http://www.gnu.org/licenses/lgpl-3.0.txt
 * @version    $Id: FileUtils.php 1869 2011-01-07 18:55:25Z pcdinh $
 */
class SpicaFileList extends SpicaAbstractDataSet
{
    /**
     * Base path this list of files.
     *
     * @var string
     */
    public $basePath;

    /**
     * Creates an object of <code>SpicaFileList</code> that represents a iterable
     * list of <code>SpicaFileEntry</code>.
     *
     * @param  string $path          Directory path
     * @param  bool   $listDirectory List sub directory or not
     * @param  bool   $listDots      List dot (.) and dot dot (..) notation or not
     */
    public function __construct($path, $listDirectory = true, $listDots = true)
    {
        // List of SpicaFileEntry(s)
        $files = array();
        $path  = str_replace('\\', '/', trim(trim($path), '\\/')); // Windows hack

        try
        {
            if (true === is_file($path))
            {
                throw new Exception('The specified path to browse is not a directory. ');
            }

            if (false === is_readable($path))
            {
                throw new Exception('Specified path "'.spica_path_make_relative($path).'" does not exist or is not accessible. ');
            }

            $iter = new DirectoryIterator($path);
            $this->basePath = $path;

            foreach ($iter as $file)
            {
                if (false === $listDots && true === $file->isDot())
                {
                    continue;
                }

                if (false === $listDirectory && true === $file->isDir())
                {
                    continue;
                }

                /**
                 * @var DirectoryIterator $file
                 */
                $entry = new SpicaFileEntry();
                $entry->fileName          = $file->getFilename();
                $entry->path              = $file->getPath();
                $entry->fullPath          = str_replace('\\', '/', $file->getPathName());
                $entry->lastModified      = $file->getMTime();
                $entry->lastInodeModified = $file->getCTime();
                $entry->lastAccessed      = $file->getATime();
                $entry->size              = $file->getSize();
                $entry->inode             = $file->getInode();
                $entry->groupName         = $file->getGroup();
                $entry->ownerName         = $file->getOwner();
                $entry->isReadable        = $file->isReadable();
                $entry->isExecutable      = $file->isExecutable();
                $entry->isWritable        = $file->isWritable();
                $entry->isDirectory       = $file->isDir();
                $entry->isDot             = $file->isDot();
                $entry->isHidden          = (0 === strpos($entry->fileName, '.') && false === $entry->isDot);
                $entry->isFile            = (!$entry->isDirectory && !$entry->isDot);
                $files[] = $entry;
            }

            $this->populate($files);
        }
        catch (Exception $ex)
        {
            $this->_readable = false;
            $this->_message  = $ex->getMessage();
        }
    }
}

/**
 * A <code>SpicaFileEntry</code> represents a file entry in a <code>SpicaFileList.</code>
 * It is used to return information about a specified file.
 *
 * namespace spica\core\utils\FileEntry
 *
 * @category   spica
 * @package    core
 * @subpackage utils
 * @author     Pham Cong Dinh <pcdinh at phpvietnam dot net>
 * @since      Version 0.3
 * @since      April 08, 2009
 * @copyright  Pham Cong Dinh (http://www.phpvietnam.net)
 * @license    http://www.gnu.org/licenses/lgpl-3.0.txt
 * @version    $Id: FileUtils.php 1869 2011-01-07 18:55:25Z pcdinh $
 */
class SpicaFileEntry
{
    /**
     * Web address of the file.
     *
     * @var string
     */
    public $url;

    /**
     * Directory path to the file entry: File name and its extension are excluded.
     *
     * @var string
     */
    public $path;

    /**
     * The fully qualified path to file including file itself.
     *
     * @var string
     */
    public $fullPath;

    /**
     * The fully qualified name including extension.
     *
     * @var string
     */
    public $fileName;

    /**
     * File size in bytes.
     *
     * @var float
     */
    public $size;

    /**
     * Inode number.
     *
     * @var int
     */
    public $inode;

    /**
     * Is a hidden file?
     *
     * @var bool
     */
    public $isHidden = false;

    /**
     * The fully qualified name.
     *
     * @var bool
     */
    public $isFile = false;

    /**
     * Is it a directory?
     *
     * @var bool
     */
    public $isDirectory = false;

    /**
     * Is it a dot link in a directory?
     *
     * @var bool
     */
    public $isDot = false;

    /**
     * Has read permission?
     *
     * @var bool
     */
    public $isReadable = true;

    /**
     * Has write permission?
     *
     * @var bool
     */
    public $isWritable = true;

    /**
     * Has delete permission?
     *
     * @var bool
     */
    public $isDeleteable = true;

    /**
     * Has execute permission?
     *
     * @var bool
     */
    public $isExecutable = true;

    /**
     * File owner name.
     *
     * @var string
     */
    public $ownerName;

    /**
     * Owner group name.
     *
     * @var string
     */
    public $groupName;

    /**
     * Link count.
     *
     * @var int
     */
    public $linkCount;

    /**
     * Last modified time.
     *
     * @var string
     */
    public $lastModified;

    /**
     * Last modified time.
     *
     * @var string
     */
    public $lastInodeModified;

    /**
     * Last accessed time.
     *
     * @var string
     */
    public $lastAccessed;
}

/**
 * Signals that an file or directory deletion exception has occurred.
 *
 * namespace spica\utils\FileDeletionException
 *
 * @category   spica
 * @package    core
 * @subpackage utils
 * @author     Pham Cong Dinh <pcdinh at phpvietnam dot net>
 * @since      Version 0.3
 * @since      April 24, 2009
 * @copyright  Pham Cong Dinh (http://www.phpvietnam.net)
 * @license    http://www.gnu.org/licenses/lgpl-3.0.txt
 * @version    $Id: FileUtils.php 1869 2011-01-07 18:55:25Z pcdinh $
 */
class SpicaFileDeletionException extends Exception
{
    /**
     * Files that failed to delete.
     *
     * @var array
     */
    protected $_files;

    /**
     * Constructs an object of <code>SpicaFileDeletionException</code>.
     *
     * @param string $message
     * @param array  $files
     * @param int    $code
     */
    public function __construct($message = null, $files = array(), $code = 0)
    {
        $this->_files = (array) $files;
        parent::__construct($message, $code);
    }
}

/**
 * Gets file MIME.
 *
 * @param  string $filePath
 * @return string
 */
function spica_file_mime($filePath)
{
    // FIXME mime_content_type() is deprecated
    if (false === function_exists('mime_content_type'))
    {
        // See http://pecl.php.net/package/Fileinfo (PHP 5.3 enables it by default)
        $fileInfo = new finfo(FILEINFO_MIME);
        $mimeType = finfo_file($fileInfo, $path);  // e.g. gives "image/jpeg"
        finfo_close($fileInfo);
        return $mimeType;
    }

    return mime_content_type($path);
}

?>